Run the app
We're going to set up the Cubist chains and relayer and then interact with our contracts.
Start Cubist chains and relayer
Now that we've built our contracts, we want to deploy them and then interact
with them. We're going to do this locally; the config (cubist-config.json
)
already specifies the URLs on which the chains will run.
The app config will look something like this:
{
...
"network_profiles": {
"default": {
"ethereum": { "url": "http://127.0.0.1:8545" },
"polygon": { "url": "http://127.0.0.1:9545" }
}
}
}
You can alter the config to specify other URLs if you choose.
To start both the local chains and the Cubist relayer, use:
cubist start
Note that this command may take a while (i.e., a minute) the first time you run it. This is because Cubist needs to download and build the local chain running services before it can start the chains.
Once you've run cubist start
, you'll see output that looks something like this:
Launching chains
ethereum ✔ [ 0s] [....................] http://localhost:8545/
polygon ✔ [ 1s] [....................] http://localhost:9545/ All servers available
Watching <path>/cubist-deploy dir
This output shows where the localnets are running (e.g., localhost:8545
),
and how long they took to become initialized and available. The output
also lets us know that the local relayer is successfully watching for the
events that tell it to relay information from one chain to another.
Alternatively, you can start the chains and the relayer seperately with
cubist chains start
and cubist relayer start
. For more information on
these and other Cubist CLI commands, try cubist help
, or check out the
CLI reference.
Right now, Avalanche and Avalanche-subnet localnets are slower to start up than other networks. We're working on it!
Run the app
Deploy
Deploy the contracts and the generated shims:
- npm
- yarn
npm run deploy 17
> [email protected] deploy
> node ./src/deploy.js
Deployed StorageReceiver
to Ethereum @ 0xaddr
Polygon @ 0xaddr
Deployed StorageSender
to Polygon @ 0xaddr
yarn deploy 17
yarn run v1.22.19
$ node ./src/deploy.js 17
Deployed StorageReceiver
to Ethereum @ 0xaddr
Polygon @ 0xaddr
Deployed StorageSender
to Polygon @ 0xaddr
✨ Done in 0.95s.
The core of the deploy script that deploy
invokes is this simple function
(which uses the Cubist ORM generated at build time):
import {
CubistORM,
} from '../build/orm/index.js';
...
// Project instance
const cubist = new CubistORM();
// Contract factories
const StorageReceiver = cubist.StorageReceiver;
const StorageSender = cubist.StorageSender;
export async function deploy(val) {
// Deploy both the StorageReceiver contract and its shim
const receiver = await StorageReceiver.deploy(val);
const senderTarget = StorageSender.target();
...
// Deploy StorageSender (it has no shim)
const sender = await StorageSender.deploy(val, receiver.addressOn(senderTarget));
...
// Return the inner ethers.js contracts
return {
receiver: receiver.inner,
sender: sender.inner
};
}
The deploy
script takes a value as its argument; when we invoked it, we
supplied 17
. First, the script deploys the StorageReceiver
contract with
val
(17
) as the constructor argument; it also implicitly deploys the StorageReceiver
shim.
Then, it deploys the StorageSender
contract; since StorageSender
is
never called from a chain other than the one on which it is deployed,
StorageSender
has no shim.
The StorageSender
deployment call (line 20) takes the receiver
address as a constructor argument:
receiver.addressOn(senderTarget)
The addressOn
call explicitly requests the address of the receiver
contract on the sender chain---in other words, it requests the
address of the StorageReceiver
's shim. In this case, since
senderTarget
---StorageSenders
's explicit target in the Cubist config
file----is Polygon, this snippet of code gets the receiver contract's
address on Polygon. If we were to change StorageSender
's target chain,
though, this code would still work; it's chain agnostic.
Here's the chain-specific version of the code:
receiver.addressOn(Polygon)
If you avoid hardcoding chain names in your off-chain code, you won't have to edit any code if you want to change chains. Instead, you'll be able to change chains by editing your configuration file.
Our scripts assume that you only deploy each contract once. For now, the Cubist Node SDK only supports single deployment. If you want to deploy multiple instances of the same contract: reach out to [email protected], say hi on discord, or check out the Rust storage app example.
Run
We can now store, increment, decrement, and retrieve the counter values:
- npm
- yarn
npm run inc 5
> node ./src/inc.js
sending StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)
Incremented counter
SENT StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)
yarn inc 5
yarn run v1.22.19
$ node ./src/inc.js 5
sending StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)
SENT StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)
Incremented counter
✨ Done in 0.84s.
This command invokes the inc
increment method to increment the counter
by 5. Let's look at the core of the inc
script that ultimately calls the smart
contract function:
import { CubistORM, } from '../build/orm/index.js';
import { fileURLToPath, } from 'url';
// Project instance
const cubist = new CubistORM();
export async function inc(val) {
const sender = (await cubist.StorageSender.deployed()).inner;
await (await sender.inc(val)).wait(1);
...
}
Once again, this script creates a new Cubist instance.
In line 8, it loads the StorageSender
contract from its deployment
receipt using the Cubist deployed
function. Then, in the next
line, it calls the deployed StorageSender
contract's inc
function using Cubist-generated bindings.
Now, when we retrieve
the new value, the increment will be reflected on
both chains:
- npm
- yarn
npm run retrieve
> [email protected] retrieve
> node ./src/retrieve.js
Receiver counter (Ethereum) = 22
Sender counter (Polygon) = 22
yarn retrieve
yarn run v1.22.19
$ node ./src/retrieve.js
Receiver counter (Ethereum) = 22
Sender counter (Polygon) = 22
✨ Done in 0.96s.
dec
and store
work similarly.
Test the app
Shut down the Cubist local nets and relayer---because the CubistTestDK handles startup and shutdown automatically---and let's look at a simple test. First, the test code creates a new TestDK instance and sets up the chains and relayer to run before the tests start, and shut down after the tests end:
const testdk = new TestDK(CubistORM, { tmp_deploy_dir: true });
const cubist = testdk.cubist;
beforeAll(async () => {
await testdk.startService();
});
afterAll(async () => {
await testdk.stopService();
});
Now we can write the code that actually tests our application. The tests
call inc
, dec
, and store
, and then use retrieve
to verify that
the values have been updated correctly:
const val = 55;
const receiver = await cubist.StorageReceiver.deploy(val);
const senderTarget = cubist.StorageSender.target();
const sender = await cubist.StorageSender.deploy(val, receiver.addressOn(senderTarget));
...
// increment counter
expect(await (await sender.inner.inc(33)).wait(/* confirmations: */ 1)).to.not.throw;
// make sure sender value is incremented by 33
expect((await sender.inner.retrieve()).eq(val + 33)).is.true;
// check that the sender value is incremented by 33
// we "tryAFewTimes" because it may take a bit for the new value to be updated on the receiver
await tryAFewTimes(async () => {
expect((await receiver.inner.retrieve()).eq(val + 33)).is.true;
});
...
This test code uses cubist
the same way that our application code did; all that's different is
that we set up the chains and relayer programmatically in the tests,
and via the command line in the actual application.
Let's run the test and see the test output:
- npm
- yarn
npm test
yarn test
The output shows that the chains and relayer, controlled by the TestDK code, actually started and stopped. It also shows the series of send and receive calls invoked by the test.
Shut down
Running cubist stop
will shut down both the chains and the relayer. Once the
chains and the relayer are shut down, trying to call contract functions from
within your dapp will result in errors.